Amazon Inspectorの検出結果をリソース単位でまとめてメール通知する方法
はじめに
以前、Amazon Inspectorの検出結果をAWS Security Hubを経由してメール通知する方法をご紹介しました。
Inspectorは脆弱性を検出するたびに結果を作成するため、1つのリソースに対して複数の脆弱性が見つかった場合、その数だけメール通知が発生してしまいます。例えば、100個の脆弱性が検出されると、100通のメールが送信される状況でした。
本記事では、脆弱性ごとの通知ではなく、1リソースごとに検出結果をまとめて1回の通知で済ませる方法をご紹介します。
構成は以下のとおりです。
本実装は、以下の流れで処理を行います。
- Inspectorで検出結果が作成され、Security Hub経由でEventBridgeが起動します。
- Firehose ストリームに検出結果が一時保存されます。
- Firehose ストリームは、指定したバッファ期間内に受信したイベントを集約し、1 つのファイルとして S3 バケットに保存します。
- 複数のリソースの複数の検出結果が集約されます。
- S3 バケットにファイルが保存されたことをトリガーにLambdaが起動します。
- Lambdaでは、S3に保存されたファイルを取得し、1リソースごとに検出結果を集約し、1リソースごとに1通のメールを送信します。
前提条件
以下の設定が完了していることを確認してください。
- AWS Security Hubの有効化
- Amazon Inspectorの有効化
- ECRとLambdaのスキャンを有効化
- EC2のスキャンは無効化(本実装では対象外)
- 検出結果保存用のS3バケットの作成
- 通知用のSNSトピックの作成
Firehose ストリーム
以下の設定でFirehose ストリームを作成します。他の設定はデフォルト値のままとします。
- ソース:Direct PUT
- 送信先:検出結果保存用のS3バケット
- バッファ間隔:1分
EventBridgeルール
EventBridgeのルールに設定するイベントパターンは以下の通りです。Severityの値は、必要に応じて変更してください。
{
"source": ["aws.securityhub"],
"detail-type": ["Security Hub Findings - Imported"],
"detail": {
"findings": {
"ProductName": ["Inspector"],
"RecordState": ["ACTIVE"],
"Severity": {
"Label": ["MEDIUM", "HIGH", "CRITICAL"]
},
"Workflow": {
"Status": ["NEW"]
}
}
}
}
ターゲットは、先程作成したFirehose ストリームを選択します。
Lambda
以下の設定でLambdaを作成します。
- ランタイム: Python 3.13
- タイムアウト: 20秒
- IAMロールに付与するポリシー
- AWSLambdaBasicExecutionRole
- 以下のIAMポリシー(SNSトピック名とS3バケット名は各自変更ください)
{
"Version": "2012-10-17",
"Statement": [
{
"Effect": "Allow",
"Action": [
"s3:GetObject"
],
"Resource": "arn:aws:s3:::YOUR-BUCKET-NAME/*"
},
{
"Effect": "Allow",
"Action": [
"sns:Publish"
],
"Resource": "arn:aws:sns:ap-northeast-1:123456789012:xxx"
}
]
}
LambdaにS3バケットのトリガーを追加します。
- S3バケット:前述の作成済みバケット
- イベントタイプ:
PUT
Lambdaのコードは以下の通りです。通知用のSNSトピックARNは、ご自身の環境に合わせて変更してください。
import json
import boto3
SNS_TOPIC_ARN = 'arn:aws:sns:ap-northeast-1:123456789012:xxx'
def parse_json_content(content):
findings_events = []
start = 0
while start < len(content):
try:
decoder = json.JSONDecoder()
obj, idx = decoder.raw_decode(content[start:])
findings_events.append(obj)
start += idx
while start < len(content) and content[start].isspace():
start += 1
except json.JSONDecodeError as e:
print(f"Error parsing JSON at position {start}: {e}")
start += 1
return findings_events
def get_resource_info(finding):
image_info = ""
resource_type = ""
if finding.get('Resources'):
resource = finding['Resources'][0]
if resource.get('Type') == 'AwsEcrContainerImage':
details = resource.get('Details', {}).get('AwsEcrContainerImage', {})
repository = details.get('RepositoryName', '不明')
tags = details.get('ImageTags', ['不明'])
image_info = f" ({repository}:{', '.join(tags)})"
resource_type = f"ECRイメージの脆弱性({repository})"
elif resource.get('Type') == 'AwsLambdaFunction':
function_name = resource.get('Details', {}).get('AwsLambdaFunction', {}).get('FunctionName', '不明')
resource_type = f"Lambda関数の脆弱性({function_name})"
return image_info, resource_type
def create_vulnerability_summary(findings):
severity_groups = {
'CRITICAL': [],
'HIGH': [],
'MEDIUM': []
}
for finding in findings:
severity = finding.get('Severity', {}).get('Label', 'UNKNOWN')
if severity in severity_groups:
vuln_id = finding.get('Title', '').split(' - ')[-1] if ' - ' in finding.get('Title', '') else finding.get('Title', '')
package_name = ''
if finding.get('Vulnerabilities', []):
vuln = finding['Vulnerabilities'][0]
if vuln.get('VulnerablePackages', []):
package_name = f" ({vuln['VulnerablePackages'][0].get('Name', '')})"
severity_groups[severity].append(f"{vuln_id}{package_name}")
return severity_groups
def format_finding_details(finding):
severity = finding.get('Severity', {}).get('Label', 'UNKNOWN')
title = finding.get('Title', 'タイトルなし')
description = finding.get('Description', '説明なし')
# CVE情報の取得
vulnerabilities = finding.get('Vulnerabilities', [])
cve_info = []
for vuln in vulnerabilities:
cve_id = vuln.get('Id', '不明なCVE')
cvss_list = vuln.get('Cvss', [])
if cvss_list:
base_score = cvss_list[0].get('BaseScore', 'N/A')
vector = cvss_list[0].get('BaseVector', 'N/A')
cve_info.append(f"{cve_id} (深刻度スコア: {base_score}, ベクター: {vector})")
else:
cve_info.append(cve_id)
# 脆弱なパッケージ情報の取得
vulnerable_packages = []
for vuln in vulnerabilities:
for pkg in vuln.get('VulnerablePackages', []):
name = pkg.get('Name', '不明')
version = pkg.get('Version', '不明')
fixed_version = pkg.get('FixedInVersion', '利用可能な修正なし')
remediation = pkg.get('Remediation', '利用可能な修正なし')
vulnerable_packages.append(
f"パッケージ名: {name}\n"
f"現在のバージョン: {version}\n"
f"修正バージョン: {fixed_version}\n"
f"修正方法: {remediation}"
)
# 修正推奨事項の取得
remediation = finding.get('Remediation', {}).get('Recommendation', {}).get('Text', '修正推奨事項なし')
return [
f"\n重要度: {severity}",
f"タイトル: {title}",
f"説明: {description}",
f"\n【CVE情報】",
'\n'.join(cve_info) if cve_info else "CVE情報なし",
f"\n【影響を受けるパッケージ】",
'\n'.join(vulnerable_packages) if vulnerable_packages else "パッケージ情報なし",
f"\n【修正推奨事項】",
remediation,
"\n" + "="*50 + "\n"
]
def create_email_message(resource_id, findings):
image_info, resource_type = get_resource_info(findings[0] if findings else {})
subject = f"Inspectorの脆弱性検出アラート - {resource_id.split('/')[-1]}{image_info}"
message_lines = [
f"Inspectorの脆弱性検出アラート",
f"リソース: {resource_id}",
f"\n{resource_type}:"
]
severity_groups = create_vulnerability_summary(findings)
for severity, vulns in severity_groups.items():
if vulns:
message_lines.append(f"- {severity} ({len(vulns)}件):")
for vuln in vulns:
message_lines.append(f" - {vuln}")
message_lines.append("")
message_lines.append("\n=== 検出詳細 ===\n")
for i, finding in enumerate(findings):
if i >= 5:
message_lines.extend([
f"\n... 他 {len(findings) - 5} 件の脆弱性があります。",
"すべての脆弱性の詳細については、AWS Security Hubのコンソールをご確認ください。",
"\n" + "="*50 + "\n"
])
break
message_lines.extend(format_finding_details(finding))
return subject[:97] + "..." if len(subject) > 100 else subject, '\n'.join(message_lines)
def lambda_handler(event, context):
print('Received event:' + json.dumps(event, ensure_ascii=False))
s3 = boto3.client('s3')
sns = boto3.client('sns')
try:
for record in event['Records']:
response = s3.get_object(
Bucket=record['s3']['bucket']['name'],
Key=record['s3']['object']['key']
)
content = response['Body'].read().decode('utf-8')
findings_events = parse_json_content(content)
resource_findings = {}
for event_data in findings_events:
for finding in event_data.get('detail', {}).get('findings', []):
for resource in finding.get('Resources', []):
resource_id = resource.get('Id', 'Unknown')
if resource_id not in resource_findings:
resource_findings[resource_id] = []
resource_findings[resource_id].append(finding)
for resource_id, findings_list in resource_findings.items():
subject, message = create_email_message(resource_id, findings_list)
sns.publish(
TopicArn=SNS_TOPIC_ARN,
Subject=subject,
Message=message
)
return {'statusCode': 200, 'body': 'Notifications sent successfully'}
except Exception as e:
print(f"Error: {str(e)}")
return {'statusCode': 500, 'body': f'Error processing findings: {str(e)}'}
本実装では、ECRイメージとLambda関数の脆弱性スキャンのみを対象としています。EC2インスタンスの脆弱性通知も必要な場合は、コード内のget_resource_info
関数に処理を追加してください。
本実装の主な特徴は以下の通りです。
- リソースごとに脆弱性をまとめて1通のメールで通知
- メール文には、重要度(CRITICAL/HIGH/MEDIUM)別にグループ化した概要を記載
- 脆弱性の詳細情報は最初の5件まで表示し、それ以降は件数のみを表示
- 重要度による表示優先順位付けは未実装
- ECRイメージとLambdaの両方に対応しており、それぞれのリソースタイプに適した内容を記載
試してみる
InspectorでECRとLambdaのスキャンを一旦無効化し、その後再度有効化すると、ECRとLambdaの全リソースに対してスキャンが実行されます。
この設定により、2件の通知メールを受信しました。
1件目は、ECRのリポジトリnginx-test
に関する検出結果です。
件名:Inspectorの脆弱性検出アラート - arn:aws:ecr:ap-northeast-1:123456789012:repository/nginx-test/sha256:37c022...
内容:
Inspectorの脆弱性検出アラート
リソース: arn:aws:ecr:ap-northeast-1:123456789012:repository/nginx-test/sha256:37c022aa2e42b98eb787cfe6be34e5457081c5b7693a4d8ea8fa43b2f07e1bbc
ECRイメージの脆弱性(nginx-test):
- CRITICAL (3件):
- expat (expat)
- expat (expat)
- aom (aom)
- HIGH (3件):
- libheif (libheif)
- pam (pam)
- expat (expat)
- MEDIUM (2件):
- libheif (libheif)
- curl (curl)
=== 検出詳細 ===
重要度: HIGH
タイトル: CVE-2023-49462 - libheif
説明: libheif v1.17.5 was discovered to contain a segmentation violation via the component /libheif/exif.cc.
【CVE情報】
CVE-2023-49462 (深刻度スコア: 8.8, ベクター: CVSS:3.1/AV:N/AC:L/PR:N/UI:R/S:U/C:H/I:H/A:H)
【影響を受けるパッケージ】
パッケージ名: libheif
現在のバージョン: 1.15.1
修正バージョン: 0:1.15.1-1+deb12u1
修正方法: apt-get update && apt-get upgrade
【修正推奨事項】
Remediation is available. Please refer to the Fixed version in the vulnerability details section above.For detailed remediation guidance for each of the affected packages, refer to the vulnerabilities section of the detailed finding JSON.
==================================================
重要度: CRITICAL
タイトル: CVE-2024-45491 - expat
説明: An issue was discovered in libexpat before 2.6.3. dtdCopy in xmlparse.c can have an integer overflow for nDefaultAtts on 32-bit platforms (where UINT_MAX equals SIZE_MAX).
【CVE情報】
CVE-2024-45491 (深刻度スコア: 9.8, ベクター: CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:H/I:H/A:H)
【影響を受けるパッケージ】
パッケージ名: expat
現在のバージョン: 2.5.0
修正バージョン: 0:2.5.0-1+deb12u1
修正方法: apt-get update && apt-get upgrade
【修正推奨事項】
Remediation is available. Please refer to the Fixed version in the vulnerability details section above.For detailed remediation guidance for each of the affected packages, refer to the vulnerabilities section of the detailed finding JSON.
==================================================
重要度: HIGH
タイトル: CVE-2024-10963 - pam
説明: A vulnerability was found in pam_access due to the improper handling of tokens in access.conf, interpreted as hostnames. This flaw allows attackers to bypass access restrictions by spoofing hostnames, undermining configurations designed to limit access to specific TTYs or services. The flaw poses a risk in environments relying on these configurations for local access control.
【CVE情報】
CVE-2024-10963 (深刻度スコア: 7.4, ベクター: CVSS:3.1/AV:N/AC:H/PR:N/UI:N/S:U/C:H/I:H/A:N)
【影響を受けるパッケージ】
パッケージ名: pam
現在のバージョン: 1.5.2
修正バージョン: NotAvailable
修正方法: NotAvailable
【修正推奨事項】
None Provided
==================================================
重要度: HIGH
タイトル: CVE-2024-45490 - expat
説明: An issue was discovered in libexpat before 2.6.3. xmlparse.c does not reject a negative length for XML_ParseBuffer.
【CVE情報】
CVE-2024-45490 (深刻度スコア: 7.5, ベクター: CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:N/I:N/A:H)
【影響を受けるパッケージ】
パッケージ名: expat
現在のバージョン: 2.5.0
修正バージョン: 0:2.5.0-1+deb12u1
修正方法: apt-get update && apt-get upgrade
【修正推奨事項】
Remediation is available. Please refer to the Fixed version in the vulnerability details section above.For detailed remediation guidance for each of the affected packages, refer to the vulnerabilities section of the detailed finding JSON.
==================================================
重要度: CRITICAL
タイトル: CVE-2024-45492 - expat
説明: An issue was discovered in libexpat before 2.6.3. nextScaffoldPart in xmlparse.c can have an integer overflow for m_groupSize on 32-bit platforms (where UINT_MAX equals SIZE_MAX).
【CVE情報】
CVE-2024-45492 (深刻度スコア: 9.8, ベクター: CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:H/I:H/A:H)
【影響を受けるパッケージ】
パッケージ名: expat
現在のバージョン: 2.5.0
修正バージョン: 0:2.5.0-1+deb12u1
修正方法: apt-get update && apt-get upgrade
【修正推奨事項】
Remediation is available. Please refer to the Fixed version in the vulnerability details section above.For detailed remediation guidance for each of the affected packages, refer to the vulnerabilities section of the detailed finding JSON.
==================================================
... 他 3 件の脆弱性があります。
すべての脆弱性の詳細については、AWS Security Hubのコンソールをご確認ください。
==================================================
2件目は、Lambdahirai-v2-lambda-function
に関する検出結果です。
件名:Inspectorの脆弱性検出アラート - sha256:37c022aa2e42b98eb787cfe6be34e5457081c5b7693a4d8ea8fa43b2f07e1bbc (ng...
内容:
Inspectorの脆弱性検出アラート
リソース: arn:aws:lambda:ap-northeast-1:123456789012:function:hirai-v2-lambda-function:$LATEST
Lambda関数の脆弱性(hirai-v2-lambda-function):
- CRITICAL (2件):
- Unsanitized input is run as code
- Hardcoded credentials
- HIGH (2件):
- OS command injection
- OS command injection
=== 検出詳細 ===
重要度: CRITICAL
タイトル: CWE-94 - Unsanitized input is run as code
説明: Running scripts generated from unsanitized inputs (for example, evaluating expressions that include user-provided strings) can lead to malicious behavior and inadvertently running code remotely.
【CVE情報】
python/code-injection@v1.0
【影響を受けるパッケージ】
パッケージ情報なし
【修正推奨事項】
Passing user-provided input to `eval` and `exec` functions without sanitization makes your code vulnerable to code injection. Make sure you implement input validation or use secure functions. [Learn more](https://cwe.mitre.org/data/definitions/94.html)
==================================================
重要度: HIGH
タイトル: CWE-77,78,88 - OS command injection
説明: Constructing operating system or shell commands with unsanitized user input can lead to inadvertently running malicious code.
【CVE情報】
python/os-command-injection@v1.0
【影響を受けるパッケージ】
パッケージ情報なし
【修正推奨事項】
subprocess call with shell=True identified, security issue. https://bandit.readthedocs.io/en/latest/plugins/b602_subprocess_popen_with_shell_equals_true.html
==================================================
重要度: HIGH
タイトル: CWE-77,78,88 - OS command injection
説明: Constructing operating system or shell commands with unsanitized user input can lead to inadvertently running malicious code.
【CVE情報】
python/os-command-injection@v1.0
【影響を受けるパッケージ】
パッケージ情報なし
【修正推奨事項】
String concatenation or formatting in calls to commands via `sh` has been detected, which can lead to Command Injection vulnerabilities. This vulnerability could allow an attacker to execute arbitrary commands on the system. To remediate this issue, avoid using string concatenation or formatting when constructing command calls. Instead, use shell argument arrays or command-specific APIs that handle command arguments securely. For more information, see [CWE-77](https://cwe.mitre.org/data/defini...Truncated
==================================================
重要度: CRITICAL
タイトル: CWE-798 - Hardcoded credentials
説明: Access credentials, such as passwords and access keys, should not be hardcoded in source code. Hardcoding credentials may cause leaks even after removing them. This is because version control systems might retain older versions of the code. Credentials should be stored securely and obtained from the runtime environment.
【CVE情報】
python/hardcoded-credentials@v1.0
【影響を受けるパッケージ】
パッケージ情報なし
【修正推奨事項】
Your code uses hardcoded AWS credentials which might allow unauthorized users access to your AWS account. These attacks can occur a long time after the credentials are removed from the code. We recommend that you set AWS credentials with environment variables or an AWS profile instead. You should consider deleting the affected account or rotating the secret key and then monitoring Amazon CloudWatch for unexpected activity.
[https://boto3.amazonaws.com/v1/documentation/api/latest/guide/credential...Truncated
==================================================
従来方式(脆弱性ごとの通知)では、上記の例において12通のメールが発生していましたが、本実装により2通に削減することができました。この改善により、以下のメリットが得られます。
-
通知の頻度低減:管理者が受け取るメールの数が大幅に減少し、重要な通知を見逃すリスクが低減されます。
-
情報の集約:1つのリソースに関する全ての脆弱性情報が1つのメールにまとまるため、リソースごとの問題把握が容易になります。
注意点として、1リソースごとに必ずしも1回の通知で完結するとは限りません。
これは、Firehoseストリームが設定されたバッファリング間隔とバッファリングサイズに厳密に従うわけではないためです。
一度のスキャンで複数の検出結果が作成された場合、Firehoseストリームはバッファ期間内でもファイルを分割して保存することがあります。その結果、Lambdaが複数回起動し、1つのリソースに対して複数回の通知が発生する可能性があります。
注:バッファリングヒントオプションはあくまでもヒントとして扱われます。Kinesis Data Firehoseは、バッファリングを最適化するために異なる値を使用することがあります。
https://repost.aws/ja/knowledge-center/kinesis-small-files-s3